package com.mebs.edi.parser.dom;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.mebs.edi.model.dom.DomAttribute;
import com.mebs.edi.model.dom.DomChild;
import com.mebs.edi.model.dom.DomChildren;
import com.mebs.edi.model.dom.DomEntity;
import com.mebs.edi.parser.utils.ClassUtils;

public class ModelToDomConverter extends DomConverter {
	private static final Logger LOG = Logger.getLogger(ModelToDomConverter.class);
	
	public ModelToDomConverter() {
		LOG.debug("Instantiating a Model to DOM converter");
	}
	
	/**
	 * 
	 * Converts an annotated class instance into a DOM Document object 
	 * 
	 * @param annotated model object
	 * @return domNode
	 * @throws ConvertionException
	 * @throws IllegalArgumentException
	 * 
	 */
	public Document convertToDocument(Object model) throws ConvertionException, IllegalArgumentException {
		try {
			//We need a Document
	        DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
	        DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
	        Document document = docBuilder.newDocument();
	        
	        Element rootElement = convert(model, document);
	        rootElement.setAttribute("xmlns:p", "http://www.w3.org/2001/XMLSchema");
	        
	        document.appendChild(rootElement);
			
			return document;
			
		} catch (Exception e) { 
			throw new ConvertionException("Convertion failed ", e);
		}
	}
	
	/**
	 * 
	 * Converts an annotated class instance into a DOM Element object 
	 * 
	 * @param annotated model object
	 * @param document
	 * @return domNode
	 * @throws ConvertionException
	 * @throws IllegalArgumentException
	 * 
	 */
	private Element convert(Object model, Document doc) throws ConvertionException, IllegalArgumentException {
		LOG.info("Converting " + model);
		
		Class modelClass = model.getClass();
		
		// argument validation, just to be sure
		DomEntity entityAnnotation = (DomEntity)modelClass.getAnnotation(DomEntity.class);
		if (entityAnnotation == null) {
			throw new IllegalArgumentException("Class " + modelClass + " does not have a DomEntity annotation");
		}
		
		try {
			String localName = entityAnnotation.name();
			String qualifiedName = "p:" + localName;
			
			Element element = doc.createElement(qualifiedName);
			
			List<Field> fields = getFields(modelClass);
			for (Field field : fields) {
				
				// mapping instance field to DOM attribute 
				DomAttribute attributeAnnotation = field.getAnnotation(DomAttribute.class);
				if (attributeAnnotation != null) {
					processAttributeFromField(field, element, model);
					continue;
				}
				
				// mapping instance field to DOM child node 
				DomChild childAnnotation = field.getAnnotation(DomChild.class);
				if (childAnnotation != null) {
					processNodeFromField(field, element, model);
					continue;
				}
				
				// mapping collection field to multiple children nodes
				DomChildren childrenAnnotation = field.getAnnotation(DomChildren.class);
				if (childrenAnnotation != null) {
					processNodesFromCollection(field, element, model);
					continue;
				}
			}
			
			return element;
			
		} catch (Exception e) { 
			throw new ConvertionException("Convertion failed ", e);
		}
	}
	
	/**
	 * 
	 * Extracts a field of 'model' object and creates a corresponding attribute of DOM element
	 * 
	 * @param field - field of the model object
	 * @param element - DOM node, which will hold the newly created attribute
	 * @param model - object, holding the field
	 * @throws Exception
	 */
	private void processAttributeFromField(Field field, Element element, Object model) throws Exception {
		String attributeName = getAttributeName(field);
		Object attributeValue = ClassUtils.getFieldValue(field, model);
		
		if (attributeValue != null) {
			String valueString = DomAttributeConverter.convertToDomAttribute(attributeValue);
			element.setAttribute(attributeName, valueString);
		}
	}
	
	/**
	 * 
	 * Extracts a field of 'model' object, converts it to a DOM node,
	 * and appends the newly created node to provided element 
	 * 
	 * @param field - field of the model object
	 * @param element - DOM node, which will hold the newly created child node
	 * @param model - object, holding the field
	 * 
	 * @throws Exception
	 */
	private void processNodeFromField(Field field, Element element, Object model) throws Exception {
		Document doc = element.getOwnerDocument();
		
		Object fieldValue = ClassUtils.getFieldValue(field, model);
		
		if (fieldValue != null) {
			Element processedNode = convert(fieldValue, doc);
			element.appendChild(processedNode);
		}
	}
	
	/**
	 * 
	 * Extracts a collection from the field of 'model' object, 
	 * converts all collection elements to DOM nodes,
	 * and appends the newly created nodes to provided element 
	 * 
	 * @param field - collection field of the model object
	 * @param element - DOM node, which will hold the newly created children nodes
	 * @param model - object, holding the field
	 * 
	 * @throws Exception
	 */
	private void processNodesFromCollection(Field field, Element element, Object model) throws Exception {
		Document doc = element.getOwnerDocument();
		
		Object fieldValue = ClassUtils.getFieldValue(field, model);
		Collection collectionField = (Collection)fieldValue;
		
		// some elements are optional, thus a collection field can left unassigned
		if (collectionField == null) {
			return;
		}
		
		for (Object collectionElement : collectionField) {
			Element processedNode = convert(collectionElement, doc);
			element.appendChild(processedNode);
		}
	}

}
